<?php

namespace Develop\Jwt;

use Exception;

class Jwt
{
    # 使用HMAC生成信息摘要时所使用的密钥
    private $key;
    # 过期时间
    private $exp;
    # 刷新时间
    private $ref;
    #  模块
    private $aud;
    # redis
    private $redis;

    /**
     * Jwt constructor.
     * @param $config 配置文件
     * @param $aud 模块名 [admin/api]
     */
    public function __construct($config, $aud)
    {
        $this->key = $config[$aud]['key'];
        $this->exp = $config[$aud]['exp'];
        $this->ref = $config[$aud]['ref'];
        $this->aud = $aud;
        $this->redis = (new RedisMode($config['redis']))->redisObj();
    }

    /**
     * 验证jwt令牌
     * @param $jwt string
     * @param $route string
     * @param string $aud string
     * author: lc
     * time: 2022-01-27
     */
    public function verifyJwt($jwt)
    {
        #  去除Bearer前缀
        $tokens = explode(' ', $jwt);
        if (count($tokens) !== 2) {
            return [
                'code' => StatusCode::FORMAT_ERROR,
                'msg' => 'Jwt令牌格式错误'
            ];
        }
        list(, $authorization) = $tokens;
        # 解析令牌
        $authorizations = explode('.', $authorization);
        if (count($authorizations) !== 3) {
            return [
                'code' => StatusCode::FORMAT_ERROR,
                'msg' => 'Jwt令牌解码错误'
            ];
        }
        list($base64header, $base64payload, $sign) = $authorizations;
        # 获取jwt算法
        try {
            $base64DecodeHeader = json_decode(
                $this->base64UrlDecode($base64header),
                TRUE
            );
            # 根据解析的jwt获取签名
            $newSign = self::signature(
                $base64header . '.' . $base64payload, $this->key, $base64DecodeHeader['alg']);
        } catch (Exception $e) {
            return [
                'code' => StatusCode::FORMAT_ERROR,
                'msg' => 'Jwt令牌参数解码错误'
            ];
        }
        # 验证令牌签名, 只要签名秘钥不泄露, 令牌就伪造不了
        if ($newSign !== $sign) {
            return [
                'code' => StatusCode::FORMAT_ERROR,
                'msg' => 'Jwt访问令牌非法'
            ];
        }
        $payload = $this->getPayload($jwt);
        if (!is_array($payload)) {
            return [
                'code' => StatusCode::FORMAT_ERROR,
                'msg' => 'Jwt令牌载荷非法'
            ];
        }
        if ($payload['aud'] !== $this->aud) {
            return [
                'code' => StatusCode::FORMAT_ERROR,
                'msg' => 'Jwt令牌模块非法'
            ];
        }
        if ($payload['iat'] > time()) {
            return [
                'code' => StatusCode::FORMAT_ERROR,
                'msg' => 'Jwt令牌签发时间非法'
            ];
        }
        # 判断令牌是否已经加入黑名单了
        $key = 'jwt' . $payload['aud'] . $payload['sub'];
        $keyTime = $this->redis->get($key);
        if ($keyTime) {
            # 验证, 黑名单时间大于签发时间, Jwt需要重新生成
            if ($keyTime >= $payload['iat']) {
                return [
                    'code' => StatusCode::LOGIN_AGAIN,
                    'msg' => 'Jwt令牌已加入黑名单'
                ];
            }
        }
        if ($payload['ref'] < time()) {
            return [
                'code' => StatusCode::LOGIN_AGAIN,
                'msg' => 'Jwt令牌超过最大刷新时间'
            ];
        }
        #   刷新令牌的时候, 不需要验证Jwt令牌是否失效, 刷新令牌的路由必须包含`refreshJwt`;
        if (strpos($_SERVER['REQUEST_URI'], "refreshJwt") === FALSE) {
            # 当路由不包含`refreshJwt`, 必须去验证令牌是否失效
            if ($payload['exp'] < time()) {
                return [
                    'code' => StatusCode::REFRESH_JWT,
                    'msg' => 'Jwt令牌失效'
                ];
            }
        }
        return [
            'code' => StatusCode::SUCCESS,
            'data' => $payload
        ];
    }

    /**
     *  解析jwt载荷
     * @param $jwt
     * author: lc
     * time: 2022-01-28
     */
    public function getPayload($jwt)
    {
        return json_decode($this->base64UrlDecode(explode('.', $jwt)[1]), TRUE);
    }

    /**
     * jwt黑名单
     * @param $jwt
     * author: lc
     * time: 2022-01-28
     */
    public function cancelJwt($jwt)
    {
        $payload = self::getPayload($jwt);
        # 旧的jwt加入黑名单列表
        self::blacklist($payload, time());
    }

    private function blacklist($payload, $time)
    {
        #   生成一个黑名单, 避免黑名单膨胀, key为用户id,value为当前时间, 有效期为刷新时间减去当前时间
        $key = 'jwt' . $payload['aud'] . $payload['sub'];
        $timeout = $payload['ref'] - $time;
        $this->redis->set($key, $time, $timeout);
    }

    /**
     * 刷新jwt
     * @param $jwt
     * author: lc
     * time: 2022-01-28
     */
    public function refreshJwt($jwt)
    {
        $time = time();
        $payload = self::getPayload($jwt);
        # 旧的jwt加入黑名单列表
        self::blacklist($payload, $time);
        #   刷新token, 需要修改签发时间,生效时间,jti标识,过期时间
        $payload['iat'] = $time;# 签发时间
        $payload['nbf'] = $time;# 生效时间
        # 过期时间
        $payload['exp'] = $time + $this->exp;
        # jti 编号 该令牌唯一标识
        $payload['jti'] = md5(uniqid('JWT') . time());
        return self::baseJwt($payload);
    }

    /**
     * 创建jwt令牌
     * @param $userId integer 令牌面向的用户
     * @param string $aud string 模块
     * author: lc
     * time: 2022-01-27
     */
    public function creatToken($userId)
    {
        # 令牌面向的用户
        $payload['sub'] = $userId;
        $time = time();
        # 签发时间
        $payload['iat'] = $time;
        # 生效时间
        $payload['nbf'] = $time;
        # 过期时间
        $payload['exp'] = $time + $this->exp;
        # 刷新时间
        $payload['ref'] = $time + $this->ref;
        # aud 受众 区分该令牌是前端api还是后端api
        $payload['aud'] = $this->aud;
        # jti 编号 该令牌唯一标识
        $payload['jti'] = md5(uniqid('JWT') . time());
        return self::baseJwt($payload);
    }

    private function baseJwt($payload)
    {
        $base64header = self::base64UrlEncode(json_encode($this->header));
        $base64payload = self::base64UrlEncode(json_encode($payload));
        $signature = self::signature(
            $base64header . '.' . $base64payload,
            $this->key,
            $this->header['alg']
        );
        return 'Bearer ' . $base64header . '.' . $base64payload . '.' . $signature;
    }

    private $header = [
        'alg' => 'HS256', //生成signature的算法
        'typ' => 'JWT'    //令牌类型
    ];

    /**
     * base64UrlEncode   https://jwt.io/  中base64UrlEncode编码实现
     * @param string $string 需要编码的字符串
     * @return string
     */
    private function base64UrlEncode(string $string)
    {
        return str_replace('=', '', strtr(base64_encode($string), '+/', '-_'));
    }

    /**
     * base64UrlEncode  https://jwt.io/  中base64UrlEncode解码实现
     * @param string $params 需要解码的字符串
     * @return bool|string
     */
    private function base64UrlDecode(string $params)
    {
        $remainder = strlen($params) % 4;
        if ($remainder) {
            $addLen = 4 - $remainder;
            $params .= str_repeat('=', $addLen);
        }
        return base64_decode(strtr($params, '-_', '+/'));
    }

    /**
     * HMACSHA256签名   https://jwt.io/  中HMACSHA256签名实现
     * @param string $params 为base64UrlEncode(header).".".base64UrlEncode(payload)
     * @param string $key
     * @param string $alg 算法方式
     * @return mixed
     */
    private function signature(string $params, string $key, string $alg)
    {
        $alg_config = array(
            'HS256' => 'sha256'
        );
        return self::base64UrlEncode(hash_hmac($alg_config[$alg], $params, $key, true));
    }
}